package br.edu.ifrn.potigol.lang;

import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;

import org.antlr.runtime.ANTLRReaderStream;
import org.antlr.runtime.CommonTokenStream;

import br.edu.ifrn.potigol.lang.nodes.Node;

/**
 * Contexto de avaliação. Determina como um nó será avaliado.
 * Um contexto mapeia variáveis ​​locais, self, e a classe atual em que métodos e constantes serão adicionados.
 * 
 * Existem três tipos diferentes de contexto:
 * 1) Na raiz do script: self = principal, class = objeto.
 * 2) Dentro do corpo de um método: self = instância da classe, class = método da classe.
 * 3) Dentro de uma definição de classe: self = classe, class = classe.
 */
public class Context {
  private PotigolObject currentSelf;
  private PotigolClass currentClass;
  private HashMap<String, PotigolObject> locals;
  
  //Um contexto pode compartilhar variáveis com um contexto pai. Exemplo: bloco try.
  @SuppressWarnings("unused")
  private Context parent;
  
  public Context(PotigolObject currentSelf, PotigolClass currentClass, Context parent) {
    this.currentSelf = currentSelf;
    this.currentClass = currentClass;
    this.parent = parent;
    if (parent == null) {
      locals = new HashMap<String, PotigolObject>();
    } else {
      locals = parent.locals;
    }
  }
  
  public Context(PotigolObject currentSelf, PotigolClass currentClass) {
    this(currentSelf, currentClass, null);
  }
  
  public Context(PotigolObject currentSelf) {
    this(currentSelf, currentSelf.getPotigolClass());
  }
  
  public Context() {
    this(PotigolRuntime.getMainObject());
  }
  
  public PotigolObject getCurrentSelf() {
    return currentSelf;
  }

  public PotigolClass getCurrentClass() {
    return currentClass;
  }
  
  public PotigolObject getLocal(String name) {
    return locals.get(name);
  }
    
  public boolean hasLocal(String name) {
    return locals.containsKey(name);
  }
    
  public void setLocal(String name, PotigolObject value) throws PotigolException {
	  if (!locals.containsKey(name)) throw new Error("A variavel " + name + " nao foi declarada!");
	  locals.put(name, value);
  }
  
  public void createLocal(String name, PotigolObject value) throws PotigolException {
	  locals.put(name, value);
  }
  
  public PotigolArrayObject getArray(String name) {
	  return (PotigolArrayObject) locals.get(name);
  }
  
  public PotigolObject getArrayValue(String name, int index) throws PotigolException {
	  PotigolArrayObject objectArray = (PotigolArrayObject) locals.get(name);
	  
	  if (index > objectArray.getArray().length -1) throw new IndexOutOfBounds(index, objectArray.getArray().length, name);
	  
	  if (index < 0) {
		  if ((index * -1) > objectArray.getArray().length -1) throw new IndexOutOfBounds(index, objectArray.getArray().length, name);
		  
		  index = (objectArray.getArray().length -1) + index;
	  }
	  
	  return objectArray.getArray()[index];
  }
  
  public void setArray(String name, PotigolArrayObject array) {
	  locals.put(name, array);
  }
  
  public void createArray(String name, Integer size, List<ValueObject> initialValues) {
	  ValueObject[] array = null;
	  if (size != null) {
		  array = new ValueObject[size];
	  }
	  
	  if (initialValues != null) {
		  for (int i=0; i < initialValues.size(); ++i) {
			  array[i] = (ValueObject) initialValues.get(i).getValue();
		  }
	  }
	  
	  PotigolArrayObject objectArray = new PotigolArrayObject();
	  objectArray.setArray(array);
	  
	  locals.put(name, objectArray);  
  }
  
  public void setArrayValue(String name, int index, PotigolObject value) {
	  PotigolArrayObject objectArray = (PotigolArrayObject) locals.get(name);
	  objectArray.getArray()[index] = value;
  }
  
  /**
   * Cria um contexto que compartilhará os atributos: locals, self e class com o contexto atual.
   */
  public Context makeChildContext() {
    return new Context(currentSelf, currentClass, this);
  }
  
  /**
   * Analisa e avalia o conteúdo do leitor (Exemplo: FileReader, StringReader).
   * @param reader	Conteúdo a ser avaliado.
   * @return Objeto resultante da avaliação.
   */
  public PotigolObject eval(Reader reader) throws PotigolException {
    try {
      PotigolLexer lexer = new PotigolLexer(new ANTLRReaderStream(reader));
      PotigolParser parser = new PotigolParser(new CommonTokenStream(lexer));
      Node node = parser.parse();
      if (node == null) return PotigolRuntime.getNil();
      return node.eval(this);
    } catch (PotigolException e) {
      throw e;
    } catch (Exception e) {
      throw new PotigolException(e);
    }
  }
  
  /**
   * Avalia o conteúdo passado como texto.
   * @see #eval(Reader)
   */
  public PotigolObject eval(String code) throws PotigolException {
    return eval(new StringReader(code));
  }
}
